# frozen_string_literal: true

class Board
  attr_reader(:b)

  def initialize(b)
    raise 'no nil Boards, sorry' unless b

    @b = b
  end

  def to_s
    @b.first.collect {'-'}.join('--').tap do |border|
      return (
        border.sub(/-/, '+').reverse + "\n" +
        @b.collect do |row|
          row.join('  ')
        end.join("\n") +
        "\n" + border
      )
    end
  end

  def vide?
    return @b.flatten.all? {|c| Game.instance.vide_char?(c)}
  end

  def flip_v
    return self.class.new(@b.reverse)
  end

  def flip_h
    return self.class.new(@b.collect(&:reverse))
  end

  def transpose
    return self.class.new(@b.transpose)
  end

  def geometry
    return {
      :rows    => @b.count,
      :columns => transpose.b.count
    }
  end

  def lift_piece(c)
    Impl.lift_piece(@b, c).tap do |h|
      return {
        :mask  => h[:mask]  && PieceMask.new(h[:mask]),
        :board => h[:board] && self.class.new(h[:board])
      }
    end
  end

  def drop_piece(mask)
    return nil unless mask

    Impl.drop_piece(@b, mask.b).tap do |b|
      return b ? self.class.new(b) : nil
    end
  end

  def move_piece(c, dir)
    lift_piece(c).tap do |lift|
      return nil if !lift[:mask] || lift[:mask].vide?

      return lift[:board].drop_piece(lift[:mask].move(dir))
    end
  end

  def legal_moves
    return %i[down right left up].product(Game.instance.piece_chars).collect do |dir, c|
      Move.new(move_piece(c, dir), c, dir)
    end.select(&:board)
  end

  def hash
    return @b.hash
  end

  def ==(other)
    return b.flatten.join == other.b.flatten.join
  end

  alias eql? ==

  class Impl
    def self.lift_piece(b, c)
      return { :mask => nil, :board => nil } if b.empty?

      [
        lift_piece_from_row(b.first, c),
        lift_piece(b.drop(1), c)
      ].tap do |head, tail|
        return {
          :mask  => [head[:mask]]  + Array(tail[:mask]),
          :board => [head[:board]] + Array(tail[:board])
        }
      end
    end

    def self.lift_piece_from_row(row, c)
      return %i[mask board].zip(
        row.collect do |e|
          [e, Game.instance.vide_char]
        end.collect do |pair|
          pair.first == c ? pair : pair.reverse
        end.transpose
      ).to_h
    end

    def self.drop_piece(b, m)
      return nil if b.nil? || m.nil?
      return [] if b.empty? || m.empty?

      return [
        drop_piece_into_row(b.first, m.first).tap do |head|
          return nil unless head
        end
      ] + Array(
        drop_piece(b.drop(1), m.drop(1)).tap do |tail|
          return nil unless tail
        end
      )
    end

    def self.drop_piece_into_row(b_row, m_row)
      return b_row.zip(m_row).collect do |be, me|
        if Game.instance.vide_char?(be)
          me
        else
          return nil unless Game.instance.vide_char?(me)

          be
        end
      end
    end
  end
end

class PieceMask < Board
  def self.nu(board, c)
    return new(
      board.b.collect do |row|
        row.collect do |e|
          e == c ? e : Game.instance.vide_char
        end
      end
    )
  end

  def piece_geometry(include_position = false)
    @b.collect do |row|
      row.reject {|e| Game.instance.vide_char?(e)}
    end.reject(&:empty?).tap do |b_piece|
      raise 'bad piece' if b_piece.collect(&:join).uniq.size > 1

      Board.new(b_piece).geometry.tap do |geom|
        return geom unless include_position

        return geom.merge(Iter.piece_position(@b))
      end
    end
  end

  class Iter
    def self.piece_position(b)
      raise 'bad mask' if b.empty?

      b.first.index {|e| !Game.instance.vide_char?(e)}.tap do |idx|
        return { :x => idx, :y => 0 } if idx

        piece_position(b.drop(1)).tap do |sub_pos|
          return {
            :x => sub_pos[:x],
            :y => sub_pos[:y] + 1
          }
        end
      end
    end
  end

  def move_up
    return nil if @b.first.any? {|c| !Game.instance.vide_char?(c)}

    return self.class.new(@b.drop(1) + [@b.first])
  end

  def move_down
    return flip_v.move_up&.flip_v
  end

  def move_left
    return transpose.move_up&.transpose
  end

  def move_right
    return transpose.move_down&.transpose
  end

  def move(dir)
    return case dir
           when :left  then move_left
           when :right then move_right
           when :up    then move_up
           when :down  then move_down
           end
  end
end

Move = Struct.new(:board, :c, :direction) do
  def self.nu_initial(board)
    return new(board, nil, nil)
  end

  def to_s
    return (c ? "(#{c} -> #{direction})" : '(start)') + "\n" + board.to_s
  end

  INVERSES = {
    :up    => :down,
    :down  => :up,
    :left  => :right,
    :right => :left
  }.freeze

  def reverse
    return self.class.new(board, c, INVERSES[direction])
  end

  def initial?
    return c.nil?
  end
end
